<?php
declare (strict_types=1);

namespace sws;

use Closure;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use ReflectionFunction;
use ReflectionFunctionAbstract;
use ReflectionMethod;
use Swoole\Coroutine;
use sws\exception\ClassNotFoundException;
use sws\exception\FuncNotFoundException;

/**
 * 容器实现协程隔离
 * 协程内优先读取协程内实例，获取失败后尝试读取非协程内数据
 * 非协程实例，对于协程内为只读模式，防止设置后，忘记销毁造成内存溢出
 *
 * Class Container
 * @package sws
 */
class Container
{
    /**
     * 容器实例
     * @var Container
     */
    protected static $instance;

    /**
     * 容器中实例对象
     * @var array
     */
    protected $instances = [];

    /**
     * 绑定标识
     * @var array
     */
    protected $bind = [];

    /**
     * 获取容器实例
     * @return Container|static
     */
    public static function getInstance(): Container
    {
        if (null === static::$instance) {
            static::$instance = new static();
        }
        return static::$instance;
    }

    /**
     * 获取顶级协程ID，非协程返回0
     * @param null $cid
     * @return int
     */
    protected function getTopCid($cid = null): int
    {
        if (null === $cid) $cid = Coroutine::getCid();
        if ($cid > 0) {
            $pid = Coroutine::getPcid($cid);
            if ($pid === -1) {
                return $cid;
            }
            return $this->getTopCid($pid);
        } else {
            return 0;
        }
    }

    /**
     * 判断是否存在容器中
     * @param $name
     * @return bool
     */
    public function has($name): bool
    {
        $cid = $this->getTopCid();
        //如果在协程内，判断协程容器和进程容器
        return $cid > 0
            ? isset($this->bind[$cid][$name]) || isset($this->instances[$cid][$name]) || isset($this->bind[0][$name]) || isset($this->instances[0][$name])
            : isset($this->bind[0][$name]) || isset($this->instances[0][$name]);
    }

    /**
     * 获取别名（递归获取，可设置别名为别名）
     * @param string $alias
     * @return string
     */
    public function getAlias(string $alias): string
    {
        $cid = $this->getTopCid();
        if (isset($this->bind[$cid][$alias]) || isset($this->bind[0][$alias])) {
            $alias = $this->bind[$cid][$alias] ?? $this->bind[0][$alias];
            if (is_string($alias)) {
                return $this->getAlias($alias);
            }
        }
        return $alias;
    }

    /**
     * 绑定别名，闭包，实例，到容器
     * @param $name
     * @param null $abstract
     */
    public function bind($name, $abstract = null): void
    {
        $cid = $this->getTopCid();
        if (is_array($name)) {
            foreach ($name as $k => $v) {
                $this->bind($k, $v);
            }
        } elseif ($abstract instanceof Closure) {
            $this->bind[$cid][$name] = $abstract;
        } elseif (is_object($abstract)) {
            $alias = $this->getAlias($name);
            $class = get_class($abstract);
            if ($class !== false && $class != $alias) {
                $this->bind($alias, $class);
                $alias = $class;
            }
            $this->instances[$cid][$alias] = $abstract;
        } else {
            $alias = $this->getAlias($name);
            if ($alias != $abstract && !is_null($abstract)) {
                $this->bind[$cid][$name] = $abstract;
            }
        }
    }

    /**
     * 获取绑定参数
     * @param ReflectionFunctionAbstract $reflect
     * @param array $var
     * @return array
     */
    protected function bindParams(ReflectionFunctionAbstract $reflect, array $var = []): array
    {
        $args = [];
        $isKey = !(array_key_first($var) === 0);

        foreach ($reflect->getParameters() as $parameter) {
            $name = $parameter->getName();
            if ($class = $parameter->getClass()) {
                $args[] = $this->getObjectParam($class->getName(), $var);
            } elseif ($isKey && isset($var[$name])) {
                $args[] = $var[$name];
            } elseif (!$isKey && !empty($var)) {
                $args[] = array_shift($var);
            } elseif ($parameter->isDefaultValueAvailable()) {
                try {
                    $args[] = $parameter->getDefaultValue();
                } catch (ReflectionException $e) {
                }
            } else {
                throw new InvalidArgumentException('method param miss:' . $name);
            }
        }
        return $args;
    }

    /**
     * 获取对象类型参数
     * @param $className
     * @param array $var
     * @return mixed
     */
    protected function getObjectParam($className, array &$var)
    {
        $array = $var;
        $value = array_shift($array);
        if ($value instanceof $className) {
            array_shift($var);
            return $value;
        }
        return $this->make($className);
    }

    /**
     * 创建类的实例 支持依赖注入
     * @param string $abstract 类名或别名
     * @param array $args 参数
     * @param bool $newInstance 是否创建新的实例
     * @return mixed|object
     */
    public function make(string $abstract, array $args = [], bool $newInstance = false)
    {
        $cid = $this->getTopCid();
        $abstract = $this->getAlias($abstract);

        //如果存在协层级容器或进程级容器直接返回
        if (isset($this->instances[$cid][$abstract]) || isset($this->instances[0][$abstract]) && !$newInstance) {
            return $this->instances[$cid][$abstract] ?? $this->instances[0][$abstract];
        }

        if (isset($this->bin[$cid][$abstract]) && $this->bind[$cid][$abstract] instanceof Closure) {
            $object = $this->invokeFunction($this->bind[$abstract], $args);
        } else {
            $object = $this->invokeClass($abstract, $args);
        }

        if (!$newInstance) {
            $this->instances[$cid][$abstract] = $object;
        }
        return $object;
    }

    /**
     * 执行函数或者闭包方法 支持参数调用
     * @param $function
     * @param array $args
     * @return mixed
     */
    public function invokeFunction($function, array $args = [])
    {
        try {
            $reflect = new ReflectionFunction($function);
        } catch (ReflectionException $e) {
            throw new FuncNotFoundException("function not exists: $function()", $function, $e);
        }
        $args = $this->bindParams($reflect, $args);
        return call_user_func_array($function, $args);
    }

    /**
     * 调用反射执行类的实例化 支持依赖注入
     * @param string $class
     * @param array $args
     * @return mixed|object
     */
    public function invokeClass(string $class, array $args = [])
    {
        try {
            $reflect = new ReflectionClass($class);
        } catch (ReflectionException $e) {
            throw new ClassNotFoundException('class not exists: ' . $class, $class, $e);
        }
        //如果有__make方法
        if ($reflect->hasMethod('__make')) {
            $method = $reflect->getMethod('__make');
            if ($method->isPublic() && $method->isStatic()) {
                $args = $this->bindParams($method, $args);
                return $method->invokeArgs(null, $args);
            }
        }
        //构造方法
        $constructor = $reflect->getConstructor();
        $args = $constructor ? $this->bindParams($constructor, $args) : [];
        return $reflect->newInstanceArgs($args);
    }

    /**
     * 调用反射执行类方法
     * @param $method
     * @param array $args
     * @return mixed
     */
    public function invokeMethod($method, array $args = [])
    {
        if (is_array($method)) {
            [$class, $method] = $method;
            $class = is_object($class) ? $class : $this->invokeClass($class);
        } else {
            [$class, $method] = explode('::', $method);
        }

        try {
            $reflect = new ReflectionMethod($class, $method);
        } catch (ReflectionException $e) {
            $class = is_object($class) ? get_class($class) : $class;
            throw new FuncNotFoundException('method not exists: ' . $class . '::' . $method . '()', "$class::$method", $e);
        }
        $args = $this->bindParams($reflect, $args);
        return $reflect->invokeArgs(is_object($class) ? $class : null, $args);
    }


    /**
     * 使用反射进行调用
     * @param $callable
     * @param array $args
     * @return mixed
     */
    public function invoke($callable, array $args = [])
    {
        if ($callable instanceof Closure) {
            return $this->invokeFunction($callable, $args);
        } elseif (is_string($callable) && false === strpos($callable, '::')) {
            return $this->invokeFunction($callable, $args);
        } else {
            return $this->invokeMethod($callable, $args);
        }
    }


    /**
     * 获取容器中实例
     * @param $abstract
     * @return mixed|object
     */
    public function get($abstract)
    {
        if ($this->has($abstract)) {
            return $this->make($abstract);
        }
        throw new ClassNotFoundException('class not exists: ' . $abstract, $abstract);
    }

    /**
     * 删除容器中实例
     * @param $abstract
     */
    public function delete($abstract)
    {
        $cid = $this->getTopCid();
        $name = $this->getAlias($abstract);
        if (isset($this->instances[$cid][$name])) {
            unset($this->instances[$cid][$name]);
        }
    }

    /**
     * 清除容器数据
     * @return void
     */
    public function clear(): void
    {
        $cid = $this->getTopCid();
        unset($this->bind[$cid]);
        unset($this->instances[$cid]);
    }

}